﻿using System;
using System.ComponentModel;
using System.Diagnostics;

namespace ZilLion.Wpf.Core.Controls.NotifyIconWpf.Interop
{
    /// <summary>
    ///     Receives messages from the taskbar icon through
    ///     window messages of an underlying helper window.
    /// </summary>
    public class WindowMessageSink : IDisposable
    {
        #region CreateMessageWindow

        /// <summary>
        ///     Creates the helper message window that is used
        ///     to receive messages from the taskbar icon.
        /// </summary>
        private void CreateMessageWindow()
        {
            //generate a unique ID for the window
            WindowId = "WPFTaskbarIcon_" + DateTime.Now.Ticks;

            //register window message handler
            messageHandler = OnWindowMessageReceived;

            // Create a simple window class which is reference through
            //the messageHandler delegate
            WindowClass wc;

            wc.style = 0;
            wc.lpfnWndProc = messageHandler;
            wc.cbClsExtra = 0;
            wc.cbWndExtra = 0;
            wc.hInstance = IntPtr.Zero;
            wc.hIcon = IntPtr.Zero;
            wc.hCursor = IntPtr.Zero;
            wc.hbrBackground = IntPtr.Zero;
            wc.lpszMenuName = "";
            wc.lpszClassName = WindowId;

            // Register the window class
            WinApi.RegisterClass(ref wc);

            // Get the message used to indicate the taskbar has been restarted
            // This is used to re-add icons when the taskbar restarts
            taskbarRestartMessageId = WinApi.RegisterWindowMessage("TaskbarCreated");

            // Create the message window
            MessageWindowHandle = WinApi.CreateWindowEx(0, WindowId, "", 0, 0, 0, 1, 1, 0, 0, 0, 0);

            if (MessageWindowHandle == IntPtr.Zero)
                throw new Win32Exception();
        }

        #endregion

        #region members

        /// <summary>
        ///     The ID of messages that are received from the the
        ///     taskbar icon.
        /// </summary>
        public const int CallbackMessageId = 0x400;

        /// <summary>
        ///     The ID of the message that is being received if the
        ///     taskbar is (re)started.
        /// </summary>
        private uint taskbarRestartMessageId;

        /// <summary>
        ///     Used to track whether a mouse-up event is just
        ///     the aftermath of a double-click and therefore needs
        ///     to be suppressed.
        /// </summary>
        private bool isDoubleClick;

        /// <summary>
        ///     A delegate that processes messages of the hidden
        ///     native window that receives window messages. Storing
        ///     this reference makes sure we don't loose our reference
        ///     to the message window.
        /// </summary>
        private WindowProcedureHandler messageHandler;

        /// <summary>
        ///     Window class ID.
        /// </summary>
        internal string WindowId { get; private set; }

        /// <summary>
        ///     Handle for the message window.
        /// </summary
        internal IntPtr MessageWindowHandle { get; private set; }

        /// <summary>
        ///     The version of the underlying icon. Defines how
        ///     incoming messages are interpreted.
        /// </summary>
        public NotifyIconVersion Version { get; set; }

        #endregion

        #region events

        /// <summary>
        ///     The custom tooltip should be closed or hidden.
        /// </summary>
        public event Action<bool> ChangeToolTipStateRequest;

        /// <summary>
        ///     Fired in case the user clicked or moved within
        ///     the taskbar icon area.
        /// </summary>
        public event Action<MouseEvent> MouseEventReceived;

        /// <summary>
        ///     Fired if a balloon ToolTip was either displayed
        ///     or closed (indicated by the boolean flag).
        /// </summary>
        public event Action<bool> BalloonToolTipChanged;

        /// <summary>
        ///     Fired if the taskbar was created or restarted. Requires the taskbar
        ///     icon to be reset.
        /// </summary>
        public event Action TaskbarCreated;

        #endregion

        #region construction

        /// <summary>
        ///     Creates a new message sink that receives message from
        ///     a given taskbar icon.
        /// </summary>
        /// <param name="version"></param>
        public WindowMessageSink(NotifyIconVersion version)
        {
            Version = version;
            CreateMessageWindow();
        }


        private WindowMessageSink()
        {
        }


        /// <summary>
        ///     Creates a dummy instance that provides an empty
        ///     pointer rather than a real window handler.<br />
        ///     Used at design time.
        /// </summary>
        /// <returns></returns>
        internal static WindowMessageSink CreateEmpty()
        {
            return new WindowMessageSink
            {
                MessageWindowHandle = IntPtr.Zero,
                Version = NotifyIconVersion.Vista
            };
        }

        #endregion

        #region Handle Window Messages

        /// <summary>
        ///     Callback method that receives messages from the taskbar area.
        /// </summary>
        private long OnWindowMessageReceived(IntPtr hwnd, uint messageId, uint wparam, uint lparam)
        {
            if (messageId == taskbarRestartMessageId)
                TaskbarCreated();

            //forward message
            ProcessWindowMessage(messageId, wparam, lparam);

            // Pass the message to the default window procedure
            return WinApi.DefWindowProc(hwnd, messageId, wparam, lparam);
        }


        /// <summary>
        ///     Processes incoming system messages.
        /// </summary>
        /// <param name="msg">Callback ID.</param>
        /// <param name="wParam">
        ///     If the version is <see cref="NotifyIconVersion.Vista" />
        ///     or higher, this parameter can be used to resolve mouse coordinates.
        ///     Currently not in use.
        /// </param>
        /// <param name="lParam">Provides information about the event.</param>
        private void ProcessWindowMessage(uint msg, uint wParam, uint lParam)
        {
            if (msg != CallbackMessageId) return;

            switch (lParam)
            {
                case 0x200:
                    MouseEventReceived(MouseEvent.MouseMove);
                    break;

                case 0x201:
                    MouseEventReceived(MouseEvent.IconLeftMouseDown);
                    break;

                case 0x202:
                    if (!isDoubleClick)
                        MouseEventReceived(MouseEvent.IconLeftMouseUp);
                    isDoubleClick = false;
                    break;

                case 0x203:
                    isDoubleClick = true;
                    MouseEventReceived(MouseEvent.IconDoubleClick);
                    break;

                case 0x204:
                    MouseEventReceived(MouseEvent.IconRightMouseDown);
                    break;

                case 0x205:
                    MouseEventReceived(MouseEvent.IconRightMouseUp);
                    break;

                case 0x206:
                    //double click with right mouse button - do not trigger event
                    break;

                case 0x207:
                    MouseEventReceived(MouseEvent.IconMiddleMouseDown);
                    break;

                case 520:
                    MouseEventReceived(MouseEvent.IconMiddleMouseUp);
                    break;

                case 0x209:
                    //double click with middle mouse button - do not trigger event
                    break;

                case 0x402:
                    BalloonToolTipChanged(true);
                    break;

                case 0x403:
                case 0x404:
                    BalloonToolTipChanged(false);
                    break;

                case 0x405:
                    MouseEventReceived(MouseEvent.BalloonToolTipClicked);
                    break;

                case 0x406:
                    ChangeToolTipStateRequest(true);
                    break;

                case 0x407:
                    ChangeToolTipStateRequest(false);
                    break;

                default:
                    Debug.WriteLine("Unhandled NotifyIcon message ID: " + lParam);
                    break;
            }
        }

        #endregion

        #region Dispose

        /// <summary>
        ///     Set to true as soon as <see cref="Dispose" />
        ///     has been invoked.
        /// </summary>
        public bool IsDisposed { get; private set; }


        /// <summary>
        ///     Disposes the object.
        /// </summary>
        /// <remarks>
        ///     This method is not virtual by design. Derived classes
        ///     should override <see cref="Dispose(bool)" />.
        /// </remarks>
        public void Dispose()
        {
            Dispose(true);

            // This object will be cleaned up by the Dispose method.
            // Therefore, you should call GC.SupressFinalize to
            // take this object off the finalization queue 
            // and prevent finalization code for this object
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        /// <summary>
        ///     This destructor will run only if the <see cref="Dispose()" />
        ///     method does not get called. This gives this base class the
        ///     opportunity to finalize.
        ///     <para>
        ///         Important: Do not provide destructors in types derived from
        ///         this class.
        ///     </para>
        /// </summary>
        ~WindowMessageSink()
        {
            Dispose(false);
        }


        /// <summary>
        ///     Removes the windows hook that receives window
        ///     messages and closes the underlying helper window.
        /// </summary>
        private void Dispose(bool disposing)
        {
            //don't do anything if the component is already disposed
            if (IsDisposed || !disposing) return;
            IsDisposed = true;

            WinApi.DestroyWindow(MessageWindowHandle);
            messageHandler = null;
        }

        #endregion
    }
}